ποΈGitΠ―ΡΠ°ποΈ
Commit 5e4cc1a386d8074b0d4c6794fcf0e4b451060c0e
Parents : 53d8908
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-07-29T15:21:42-05:00
Committer : GitHub <noreply@github.com>
Date : 2026-07-29T15:21:42-05:00
docs: generate Obtainium deep links, and keep them fresh automatically (#6516)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Changes
10 files changed, 568 insertions(+), 119 deletions(-)
Diff
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index d48f35c560..c6b5115115 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -200,6 +200,14 @@ jobs:
# the Crowdin post-export processor.
- name: Check for escaped quotes in base string resources
run: python3 scripts/check-string-escapes.py
+ # The deep-link tables (README + developer guide) and both Obtainium import
+ # files are generated from CHANNELS x FLAVORS in obtainium/generate-links.py.
+ # Offline and deterministic, so drift can only come from a commit: this fails
+ # the PR that hand-edits a generated file or changes the script without
+ # regenerating. The network half (are the APK filters still matching real
+ # release assets?) is the --refresh probe in scheduled-updates.yml.
+ - name: Check Obtainium generated links are current
+ run: python3 obtainium/generate-links.py --check
# Flathub/AppStream needs a <release> entry for the version being shipped; a stale
# <releases> block degrades (or fails) the Flathub listing. Fails the PR that bumps
# VERSION_NAME_BASE until the matching entry is added.
diff --git a/.github/workflows/scheduled-updates.yml b/.github/workflows/scheduled-updates.yml
index f13d67cbec..e163f858a3 100644
--- a/.github/workflows/scheduled-updates.yml
+++ b/.github/workflows/scheduled-updates.yml
@@ -161,6 +161,32 @@ jobs:
fi
fi
+ - name: Refresh Obtainium configs and deep links
+ id: obtainium
+ env:
+ # Only for the API rate limit; the releases endpoint is public.
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ # Regenerates the deep-link tables and import files, then probes the live
+ # releases to confirm every apkFilterRegEx still matches published assets.
+ # A non-zero exit means an asset was renamed and the configs are broken β
+ # surface it as an error and skip the update rather than committing configs
+ # that resolve to nothing. Unreachable API exits 0 without touching files.
+ if python3 obtainium/generate-links.py --refresh; then
+ if git diff --quiet -- README.md docs/en/developer/test-builds.md obtainium; then
+ echo "No changes detected in Obtainium configs."
+ echo "status=unchanged" >> "$GITHUB_OUTPUT"
+ else
+ echo "Obtainium configs or channel status changed."
+ echo "status=updated" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ echo "::warning::Obtainium config refresh failed β see the error above."
+ echo "status=error" >> "$GITHUB_OUTPUT"
+ echo "detail=an apkFilterRegEx no longer matches the published release assets" >> "$GITHUB_OUTPUT"
+ git checkout -- README.md docs/en/developer/test-builds.md obtainium
+ fi
+
- name: Sync with Crowdin
uses: crowdin/github-action@v2
with:
@@ -206,6 +232,8 @@ jobs:
EVENT_FIRMWARE_DETAIL: ${{ steps.event_firmware.outputs.detail }}
DEVICE_LINKS_STATUS: ${{ steps.device_links.outputs.status }}
DEVICE_LINKS_DETAIL: ${{ steps.device_links.outputs.detail }}
+ OBTAINIUM_STATUS: ${{ steps.obtainium.outputs.status }}
+ OBTAINIUM_DETAIL: ${{ steps.obtainium.outputs.detail }}
run: |
firmware_status="$FIRMWARE_STATUS"
firmware_detail="$FIRMWARE_DETAIL"
@@ -215,6 +243,8 @@ jobs:
event_firmware_detail="$EVENT_FIRMWARE_DETAIL"
device_links_status="$DEVICE_LINKS_STATUS"
device_links_detail="$DEVICE_LINKS_DETAIL"
+ obtainium_status="$OBTAINIUM_STATUS"
+ obtainium_detail="$OBTAINIUM_DETAIL"
body="This PR includes automated updates from the scheduled workflow:"
body+=$'\n'
@@ -251,6 +281,14 @@ jobs:
*) body+=$'\n'"- β \`device_links.json\` β unknown status." ;;
esac
+ # Obtainium configs / deep links
+ case "$obtainium_status" in
+ updated) body+=$'\n'"- β
Obtainium deep links and import files regenerated (channel status changed)." ;;
+ unchanged) body+=$'\n'"- βοΈ Obtainium configs checked against the live releases β no changes detected." ;;
+ error) body+=$'\n'"- β οΈ Obtainium configs skipped β ${obtainium_detail}." ;;
+ *) body+=$'\n'"- β Obtainium configs β unknown status." ;;
+ esac
+
# Crowdin (always attempted)
body+=$'\n'"- Source strings were uploaded to Crowdin."
body+=$'\n'"- Latest translations were downloaded from Crowdin (if available)."
@@ -278,6 +316,7 @@ jobs:
- Device links list
- Crowdin source string uploads
- Crowdin translation downloads
+ - Obtainium deep links and import files
title: 'chore: Scheduled updates (Firmware, Hardware, Translations)'
body: ${{ steps.pr_body.outputs.content }}
branch: 'scheduled-updates'
@@ -291,6 +330,8 @@ jobs:
fastlane/metadata/android/**
**/strings.xml
docs/**/*.md
+ README.md
+ obtainium/**
labels: |
automation
l10n
diff --git a/README.md b/README.md
index fa42d53d96..8338fe60d2 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,31 @@ Highlights from the 2.8.0 release:
## Get Meshtastic
-The easiest and fastest way to get the latest releases is to use our [GitHub releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommended to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates automatically β ready-made configurations for every channel (stable, beta, snapshot) are in [`obtainium/`](obtainium/), and [Test Builds & Obtainium](docs/en/developer/test-builds.md) explains the setup.
+The easiest and fastest way to get the latest releases is to use our [GitHub releases](https://github.com/meshtastic/Meshtastic-Android/releases). It is recommended to use these with [Obtainium](https://github.com/ImranR98/Obtainium) to get the latest updates automatically.
+
+With Obtainium installed, tap a link below on your phone to set it up with everything pre-configured. The `google` flavor adds Google push (FCM) and Google Maps; `fdroid` has no Google dependencies.
+
+<!-- BEGIN GENERATED LINKS: obtainium/generate-links.py -->
+
+| Channel | `google` flavor | `fdroid` flavor |
+|---|---|---|
+| **Latest release** | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%22%2C%22additionalSettings%22%3A%22%7B%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%22%2C%22additionalSettings%22%3A%22%7B%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%5C%22%7D%22%7D) |
+| **Open beta** | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-open%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-open%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) |
+
+<!-- END GENERATED LINKS -->
+
+What those two channels point at right now:
+
+<!-- BEGIN GENERATED STATUS: obtainium/generate-links.py --refresh -->
+
+| Channel | Currently | Released |
+|---|---|---|
+| **Latest release** | `v2.7.14` | 2026-06-03 |
+| **Open beta** | `v2.8.0-open.6` | 2026-07-29 |
+
+<!-- END GENERATED STATUS -->
+
+Closed-beta and per-commit snapshot channels, importable config files, and the setup details are in [Test Builds & Obtainium](docs/en/developer/test-builds.md). These links, files and the table above are generated β see [`obtainium/`](obtainium/).
Alternatively, these other providers are also available, but may be slower to update.
diff --git a/docs/en/developer/test-builds.md b/docs/en/developer/test-builds.md
index 21e937c05b..6448059efe 100644
--- a/docs/en/developer/test-builds.md
+++ b/docs/en/developer/test-builds.md
@@ -32,7 +32,11 @@ Meshtastic for Android promotes builds up a ladder: `closed β open β product
| **closed** beta | Alpha (Closed) | published prerelease, tag `vX.Y.Z-closed.N` | β
Yes |
| **snapshot** | β (not on Play) | rolling prerelease, tag `snapshot` | β
Yes |
-Each promotion cuts its **own** tag, and older ones stay published β `v2.8.0-open.1` through `v2.8.0-open.6` all exist as installable releases with APKs attached. So a channel accumulates releases during a version cycle rather than moving a single one forward, and the channel configs below pick the **newest** release whose title matches the channel.
+Each promotion cuts its **own** tag, and older ones stay published β a whole run of `vX.Y.Z-open.N` tags accumulates during a version cycle, each still installable with APKs attached. So a channel gathers releases rather than moving a single one forward, and the channel configs below pick the **newest** release whose title matches the channel.
+
+Which release each channel currently resolves to is deliberately **not** listed on this page: it is archived per release tag and bundled into the app, so a live version number here would be frozen at build time and wrong within weeks. The [project README](https://github.com/meshtastic/Meshtastic-Android#get-meshtastic) carries the current state, refreshed automatically.
+
+A channel can legitimately be empty β most often `-closed`, early in a cycle before any build has been cut to it. The number of published `fdroid` ABI splits has also changed over time (2.7.14 shipped five, 2.8.0 ships three), which is why the filter matches every split rather than naming ABIs.
**Snapshot** is different: it's an automated debug build of the latest commit on `main`, rebuilt and re-published under the single moving `snapshot` tag on every push. It never goes to Play. Because debug builds use a `.debug` application-ID suffix (`com.geeksville.mesh.fdroid.debug` / `com.geeksville.mesh.google.debug`) and the debug signing key, a snapshot installs as its **own separate app** β it sits alongside a Play/stable/beta install, so the uninstall-first warning above does **not** apply to it.
@@ -40,13 +44,36 @@ Each promotion cuts its **own** tag, and older ones stay published β `v2.8.0-o
## One-tap setup
-Prebuilt configurations live in [`obtainium/`](https://github.com/meshtastic/Meshtastic-Android/tree/main/obtainium) β every setting below is already baked in. Either import [`meshtastic-obtainium-export.json`](https://github.com/meshtastic/Meshtastic-Android/blob/main/obtainium/meshtastic-obtainium-export.json) via **Import/Export β Obtainium import**, or tap a channel link generated by:
+Tap a link below on the device that has Obtainium installed. Every setting from the rest of this page is already baked into it β pick the channel you want and the flavor you want ([which flavor?](#picking-the-apk)).
+
+<!-- BEGIN GENERATED LINKS: obtainium/generate-links.py -->
+
+| Channel | `google` flavor | `fdroid` flavor |
+|---|---|---|
+| Stable | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%22%2C%22additionalSettings%22%3A%22%7B%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%22%2C%22additionalSettings%22%3A%22%7B%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%5C%22%7D%22%7D) |
+| Open beta | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-open%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-open%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) |
+| Closed beta | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Alpha%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-closed%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Alpha%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Alpha%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-closed%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Alpha%5C%22%7D%22%7D) |
+| Bleeding edge (newest promoted test build) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-%28closed%7Copen%29%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22google-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Beta%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22-%28closed%7Copen%29%5C%22%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-release%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Beta%5C%22%7D%22%7D) |
+| Snapshot (latest commit on `main`) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh.google.debug%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Snapshot%20%28google%29%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5ESnapshot%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Atrue%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Atrue%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22google-.%2A-debug-%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Snapshot%20%28google%29%5C%22%7D%22%7D) | [Add](https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/%7B%22id%22%3A%22com.geeksville.mesh.fdroid.debug%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fmeshtastic%2FMeshtastic-Android%22%2C%22author%22%3A%22meshtastic%22%2C%22name%22%3A%22Meshtastic%20Snapshot%20%28fdroid%29%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5ESnapshot%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Atrue%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22releaseDateAsVersion%5C%22%3Atrue%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22fdroid-.%2A-debug-%5C%5C%5C%5Cd%2B%5C%5C%5C%5C.apk%24%5C%22%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22Meshtastic%20Snapshot%20%28fdroid%29%5C%22%7D%22%7D) |
+
+<!-- END GENERATED LINKS -->
-```bash
-python3 obtainium/generate-links.py
-```
+Prefer a file? Import one of these via **Import/Export β Obtainium import**. Pick the one matching the flavor you want for the *release* build; each also sets up **both** snapshot flavors, which can coexist:
+
+- [`meshtastic-obtainium-export-google.json`](https://github.com/meshtastic/Meshtastic-Android/blob/main/obtainium/meshtastic-obtainium-export-google.json) β `google` stable + both snapshots
+- [`meshtastic-obtainium-export-fdroid.json`](https://github.com/meshtastic/Meshtastic-Android/blob/main/obtainium/meshtastic-obtainium-export-fdroid.json) β `fdroid` stable + both snapshots
+
+### How many entries can you have at once?
+
+Obtainium keys apps by application ID, so the rule follows from ours:
+
+| | Application ID | Can coexist? |
+|---|---|---|
+| Release builds β stable, open, closed, bleeding edge, either flavor | `com.geeksville.mesh` | **No** β pick exactly one |
+| Snapshot, `google` | `com.geeksville.mesh.google.debug` | Yes |
+| Snapshot, `fdroid` | `com.geeksville.mesh.fdroid.debug` | Yes |
-Every channel except snapshot is the same `com.geeksville.mesh` package, and Obtainium keys apps by package name β so you can follow **one** of stable/open/closed/bleeding-edge at a time, plus snapshot alongside it. Tapping a second channel link switches the existing entry over rather than adding a new one.
+So one release entry, plus up to both snapshots, is the maximum. Tapping a second *release* link switches that single entry to the new channel or flavor instead of adding one, and importing both files in turn leaves you with whichever flavor's release entry came last. The snapshots are unaffected either way.
To set things up by hand instead, follow the rest of this page.
@@ -112,7 +139,7 @@ Two more things about release selection apply to every channel above, not just s
> **Leave "Fallback to older releases" ON.** It is not a channel-strictness setting, and turning it off breaks every channel config here. Obtainium's release loop breaks out after the first candidate when fallback is off, so a title filter never gets to skip past releases that don't match β a config pinned to `-open` or `^Snapshot` then fails with "Could not find a suitable release" because production `v2.7.14` sits at the head of the list. The title filter is what pins the channel; fallback is what lets Obtainium walk down to the newest release that matches it.
>
-> **If your channel filter still finds nothing:** that channel genuinely has no published release yet (common early in a version cycle, and always true for `-closed` once builds have moved to `-open`). Use the *Bleeding edge* form above to follow whichever channel is currently live.
+> **If your channel filter still finds nothing:** that channel has no published release yet β common early in a version cycle. Check the [project README](https://github.com/meshtastic/Meshtastic-Android#get-meshtastic) for what is live, or use the *Bleeding edge* form to follow whichever of open/closed is newest.
---
diff --git a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
index 8197b76a9f..24854ca249 100644
--- a/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
+++ b/feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt
@@ -596,7 +596,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
10,
listOf("obtainium", "beta", "snapshot", "apk", "install", "channel", "prerelease"),
listOf("test-builds", "obtainium", "beta", "snapshot"),
- 11600,
+ 18000,
"test-builds",
),
)
diff --git a/obtainium/README.md b/obtainium/README.md
index 64b299ea5a..63fa80abd4 100644
--- a/obtainium/README.md
+++ b/obtainium/README.md
@@ -6,16 +6,47 @@ config site expect. Setup instructions and channel reference live in
[docs/en/developer/test-builds.md](../docs/en/developer/test-builds.md); this
directory holds the machine-readable artifacts.
-| File | What it is |
-|---|---|
-| `com.geeksville.mesh.json` | Submission file for [apps.obtainium.imranr.dev](https://github.com/ImranR98/apps.obtainium.imranr.dev) β stable channel, both flavors |
-| `meshtastic-obtainium-export.json` | Obtainium **Import/Export β Obtainium import** file (stable + snapshot) |
-| `generate-links.py` | Emits `obtainium://app/β¦` one-tap links for every channel |
+| File | What it is | Generated? |
+|---|---|---|
+| `generate-links.py` | Source of truth for every channel/flavor config | β |
+| `meshtastic-obtainium-export-google.json` | **Import/Export β Obtainium import** file: `google` release + both snapshots | β
|
+| `meshtastic-obtainium-export-fdroid.json` | Same, with the `fdroid` release | β
|
+| the deep-link table in [test-builds.md](../docs/en/developer/test-builds.md) | `obtainium://app/β¦` one-tap links, every channel Γ both flavors | β
|
+| the deep-link table in [README.md](../README.md) | same links, narrowed to latest release + open beta Γ both flavors | β
|
+| `com.geeksville.mesh.json` | Submission file for [apps.obtainium.imranr.dev](https://github.com/ImranR98/apps.obtainium.imranr.dev) | hand-maintained |
+
+`generate-links.py` writes the three generated targets from one `CHANNELS` Γ
+`FLAVORS` definition, so the links and the import files cannot disagree:
```bash
-python3 obtainium/generate-links.py
+python3 obtainium/generate-links.py # regenerate
+python3 obtainium/generate-links.py --check # verify, exits 1 on drift
```
+Run `--check` after editing anything in this directory. Editing a generated file
+by hand is always wrong β change `generate-links.py` and regenerate.
+
+### How many entries a user can have
+
+Obtainium keys apps by application ID, and that governs the whole layout:
+
+| | Application ID | Coexist? |
+|---|---|---|
+| Release builds β stable, open, closed, bleeding edge, either flavor | `com.geeksville.mesh` | **No**, one only |
+| Snapshot `google` | `com.geeksville.mesh.google.debug` | Yes |
+| Snapshot `fdroid` | `com.geeksville.mesh.fdroid.debug` | Yes |
+
+So each import file carries exactly **one** release entry β hence one file per
+flavor β plus **both** snapshots. Import is `saveApps()` keyed by `id`, so two
+release entries in one file would silently overwrite rather than error. The beta
+channels are deliberately left to the deep links for the same reason.
+
+Only the snapshot labels carry a flavor suffix (`Meshtastic Snapshot (fdroid)`),
+because only those can coexist and need telling apart. Labels are set via
+`appName`, not `name`: `App.finalName` is `additionalSettings['appName'] ?? name`,
+and without `appName` Obtainium falls back to the installed app's own label β a
+debug build shows up as "Google Debug".
+
> `com.geeksville.mesh.json` is a **byte-identical mirror** of what we submitted
> upstream as [apps.obtainium.imranr.dev#1566](https://github.com/ImranR98/apps.obtainium.imranr.dev/pull/1566)
> (`public/data/apps/complex/com.geeksville.mesh.json`). Change it here and
@@ -56,18 +87,11 @@ Three distribution shapes, all carrying the same object:
form. `categories` must come from
[`public/data/categories.json`](https://github.com/ImranR98/apps.obtainium.imranr.dev/blob/main/public/data/categories.json)
β `messaging` for us.
-- **Import/export file** β `{ "apps": [ β¦ ], "settings": null }`. One entry per
- package name, because Obtainium keys apps by `id`.
-
- This is a real constraint on how many channels a user can follow at once.
- Every non-debug channel β stable `google`, stable `fdroid`, open beta, closed
- beta, bleeding edge β is `com.geeksville.mesh`, so **only one of them can be
- tracked at a time**. Adding a second reuses that one entry and switches which
- channel it follows; it does not create a parallel entry. That is why the
- export ships stable plus snapshot and not stable plus a beta channel. Only
- snapshot escapes it, via the `.debug` application-ID suffix
- (`com.geeksville.mesh.google.debug`), so it can sit alongside a release
- install.
+- **Import/export file** β `{ "apps": [ β¦ ], "settings": null }`, one entry per
+ application ID (see [above](#how-many-entries-a-user-can-have)). The legacy
+ shape without `schemaVersion` is deliberate: Obtainium reads it through the
+ `schemaVersion`-absent branch, which avoids inventing `exportedAt`/`appVersion`
+ provenance for a file no Obtainium install actually produced.
- **Deep link** β `obtainium://app/<percent-encoded config JSON>` adds the app
with settings baked in. `obtainium://add/<url>` only prefills the Add-App page.
Wrapping it as `https://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/β¦`
diff --git a/obtainium/generate-links.py b/obtainium/generate-links.py
index de8e390efe..78f28882e3 100644
--- a/obtainium/generate-links.py
+++ b/obtainium/generate-links.py
@@ -1,93 +1,166 @@
#!/usr/bin/env python3
-"""Emit Obtainium one-tap install links for the Meshtastic channel configs.
+"""Generate the Obtainium artifacts from one source of truth.
-Obtainium's `obtainium://app/<url-encoded config JSON>` scheme adds an app with
-its settings baked in. The apps.obtainium.imranr.dev redirect wrapper exists so
-the link survives being pasted somewhere that won't linkify a custom scheme.
+This script owns:
-Usage:
- python3 obtainium/generate-links.py # markdown table
- python3 obtainium/generate-links.py --json # raw config JSON per channel
+ 1. the one-tap deep-link tables in the project README and the developer guide,
+ 2. the per-flavor Obtainium import files in this directory, and
+ 3. with --refresh, the "currently published" channel table in the guide.
+
+Two kinds of staleness, two modes:
+
+ python3 obtainium/generate-links.py # write offline targets
+ python3 obtainium/generate-links.py --check # verify offline targets, exit 1 on drift
+ python3 obtainium/generate-links.py --refresh # + hit the releases API (network)
+
+--check is deterministic and offline: the links and import files derive purely
+from CHANNELS x FLAVORS, so they can only drift when a commit changes them. That
+is a pull-request concern.
+
+--refresh is the part that goes stale on its own. It resolves each channel
+against the live GitHub releases and asserts every `apkFilterRegEx` still matches
+the assets our release workflows actually publish β so renaming a release asset
+fails loudly here instead of silently breaking every user's update check. It also
+records which channels are currently published, which changes as builds are
+promoted. That is a scheduled concern; see scheduled-updates.yml.
+
+`com.geeksville.mesh.json` is deliberately NOT generated here: it is a
+byte-identical mirror of what we submitted to apps.obtainium.imranr.dev and is
+maintained by hand alongside that PR.
"""
import argparse
import json
+import os
+import re
import sys
+import urllib.error
+import urllib.request
+from pathlib import Path
from urllib.parse import quote
REPO_URL = "https://github.com/meshtastic/Meshtastic-Android"
REDIRECT = "https://apps.obtainium.imranr.dev/redirect.html?r="
-# Release assets are named androidApp-<flavor>[-<abi>]-release.apk; snapshot
-# builds attach androidApp-<flavor>-<abi>-debug-<versionCode>.apk instead.
-GOOGLE_APK = r"google-release\.apk$"
-FDROID_APK = r"fdroid-.*-release\.apk$"
-SNAPSHOT_APK = r"google-.*-debug-\d+\.apk$"
+REPO_ROOT = Path(__file__).resolve().parent.parent
+DOC = REPO_ROOT / "docs/en/developer/test-builds.md"
+README = REPO_ROOT / "README.md"
+OBTAINIUM_DIR = REPO_ROOT / "obtainium"
-CHANNELS = [
- (
- "Stable (Google flavor)",
- "com.geeksville.mesh",
- "Meshtastic",
- {"apkFilterRegEx": GOOGLE_APK},
- ),
- (
- "Stable (F-Droid flavor)",
- "com.geeksville.mesh",
- "Meshtastic",
- {"apkFilterRegEx": FDROID_APK, "autoApkFilterByArch": True},
- ),
- (
- "Open beta",
- "com.geeksville.mesh",
- "Meshtastic Beta",
- {
- "includePrereleases": True,
- "filterReleaseTitlesByRegEx": "-open",
- "apkFilterRegEx": GOOGLE_APK,
+BEGIN = "<!-- BEGIN GENERATED LINKS: obtainium/generate-links.py -->"
+END = "<!-- END GENERATED LINKS -->"
+
+STATUS_BEGIN = "<!-- BEGIN GENERATED STATUS: obtainium/generate-links.py --refresh -->"
+STATUS_END = "<!-- END GENERATED STATUS -->"
+
+RELEASES_API = "https://api.github.com/repos/meshtastic/Meshtastic-Android/releases?per_page=100"
+
+# The developer guide documents every channel; the project README only carries
+# the two channels a normal user should be choosing between.
+README_CHANNELS = ("stable", "open")
+
+# Release assets are androidApp-<flavor>[-<abi>]-release.apk; snapshot builds
+# attach androidApp-<flavor>-<abi>-debug-<versionCode>.apk instead. The google
+# flavor ships a single universal release APK, so it needs no arch filter; the
+# fdroid flavor is split per ABI and lets Obtainium's arch filter choose.
+FLAVORS = {
+ "google": {
+ "label": "google",
+ "release_apk": {"apkFilterRegEx": r"google-release\.apk$"},
+ "debug_apk": {
+ "apkFilterRegEx": r"google-.*-debug-\d+\.apk$",
+ "autoApkFilterByArch": True,
},
- ),
- (
- "Closed beta",
- "com.geeksville.mesh",
- "Meshtastic Alpha",
- {
- "includePrereleases": True,
- "filterReleaseTitlesByRegEx": "-closed",
- "apkFilterRegEx": GOOGLE_APK,
+ "debug_id": "com.geeksville.mesh.google.debug",
+ },
+ "fdroid": {
+ "label": "fdroid",
+ "release_apk": {
+ "apkFilterRegEx": r"fdroid-.*-release\.apk$",
+ "autoApkFilterByArch": True,
},
- ),
- (
- "Bleeding edge (newest promoted test build)",
- "com.geeksville.mesh",
- "Meshtastic Beta",
- {
+ "debug_apk": {
+ "apkFilterRegEx": r"fdroid-.*-debug-\d+\.apk$",
+ "autoApkFilterByArch": True,
+ },
+ "debug_id": "com.geeksville.mesh.fdroid.debug",
+ },
+}
+
+RELEASE_ID = "com.geeksville.mesh"
+
+# Every non-debug channel is RELEASE_ID, so only one of them can be tracked at a
+# time. `debug` marks the snapshot channel, which gets its own application ID.
+CHANNELS = [
+ {"key": "stable", "label": "Stable", "name": "Meshtastic", "settings": {}},
+ {
+ "key": "open",
+ "label": "Open beta",
+ "name": "Meshtastic Beta",
+ "settings": {"includePrereleases": True, "filterReleaseTitlesByRegEx": "-open"},
+ },
+ {
+ "key": "closed",
+ "label": "Closed beta",
+ "name": "Meshtastic Alpha",
+ "settings": {"includePrereleases": True, "filterReleaseTitlesByRegEx": "-closed"},
+ },
+ {
+ "key": "bleeding",
+ "label": "Bleeding edge (newest promoted test build)",
+ "name": "Meshtastic Beta",
+ "settings": {
"includePrereleases": True,
"filterReleaseTitlesByRegEx": "-(closed|open)",
- "apkFilterRegEx": GOOGLE_APK,
},
- ),
- (
- "Snapshot (latest commit on main)",
- "com.geeksville.mesh.google.debug",
- "Meshtastic Snapshot",
- {
+ },
+ {
+ "key": "snapshot",
+ "label": "Snapshot (latest commit on `main`)",
+ "name": "Meshtastic Snapshot",
+ "debug": True,
+ "settings": {
"includePrereleases": True,
"filterReleaseTitlesByRegEx": "^Snapshot",
"useLatestAssetDateAsReleaseDate": True,
- # The tag never moves off "snapshot", so date is the only usable version.
+ # The tag never moves off "snapshot", so the release date is the only
+ # thing that changes between builds.
"versionDetection": False,
"releaseDateAsVersion": True,
- "apkFilterRegEx": SNAPSHOT_APK,
- "autoApkFilterByArch": True,
},
- ),
+ },
]
+# What each per-flavor import file carries.
+#
+# Release builds of both flavors are RELEASE_ID, so an import file can only ever
+# carry ONE of them β Obtainium's import calls saveApps() keyed by id, and a
+# second entry with the same id silently overwrites the first. That is why the
+# flavor choice is expressed as two separate files, and why the beta channels
+# (also RELEASE_ID) are left to the deep links rather than bundled here.
+#
+# Debug builds are the exception: they carry a per-flavor `.debug` suffix, so
+# both snapshots are genuinely distinct apps and every file ships both.
+EXPORT_RELEASE_CHANNEL = "stable"
+EXPORT_DEBUG_CHANNELS = ("snapshot",)
+
-def config_for(app_id, name, settings):
+def config_for(channel, flavor_key):
+ """Build one Obtainium app config for a channel/flavor pair."""
+ flavor = FLAVORS[flavor_key]
+ is_debug = channel.get("debug", False)
+ settings = dict(channel["settings"])
+ settings.update(flavor["debug_apk"] if is_debug else flavor["release_apk"])
+ # Flavor-suffix only the snapshots. Both debug flavors can coexist, so their
+ # labels have to disambiguate; a release entry is always singular, so
+ # "Meshtastic" beats branding it with a build-flavor suffix forever.
+ name = f"{channel['name']} ({flavor['label']})" if is_debug else channel["name"]
+ # appName is the only reliable label: App.finalName is
+ # `additionalSettings['appName'] ?? name`, and Obtainium otherwise falls back
+ # to the installed app's own label (a debug build shows up as "Google Debug").
+ settings["appName"] = name
return {
- "id": app_id,
+ "id": flavor["debug_id"] if is_debug else RELEASE_ID,
"url": REPO_URL,
"author": "meshtastic",
"name": name,
@@ -95,24 +168,243 @@ def config_for(app_id, name, settings):
}
+def deep_link(channel, flavor_key):
+ blob = json.dumps(config_for(channel, flavor_key), separators=(",", ":"))
+ # Only the JSON payload is percent-encoded; the scheme prefix stays literal.
+ return f"{REDIRECT}obtainium://app/{quote(blob, safe='')}"
+
+
+def render_table(channels=None, label_of=None):
+ """Render the marker-delimited link table for `channels` (default: all)."""
+ channels = channels or CHANNELS
+ lines = [
+ BEGIN,
+ "",
+ "| Channel | `google` flavor | `fdroid` flavor |",
+ "|---|---|---|",
+ ]
+ for channel in channels:
+ cells = " | ".join(
+ f"[Add]({deep_link(channel, flavor_key)})" for flavor_key in FLAVORS
+ )
+ label = label_of(channel) if label_of else channel["label"]
+ lines.append(f"| {label} | {cells} |")
+ lines += ["", END]
+ return "\n".join(lines)
+
+
+def replace_block(text, body, begin=BEGIN, end=END):
+ """Replace the marker-delimited block in `text` with `body`."""
+ start, stop = text.index(begin), text.index(end) + len(end)
+ return text[:start] + body + text[stop:]
+
+
+# --- live release data (--refresh only) -------------------------------------
+
+
+def fetch_releases():
+ request = urllib.request.Request(
+ RELEASES_API, headers={"Accept": "application/vnd.github+json"}
+ )
+ # Authenticate when a token is present purely for the rate limit; the data is
+ # public either way, so this still works locally with no token.
+ token = os.environ.get("GITHUB_TOKEN")
+ if token:
+ request.add_header("Authorization", f"Bearer {token}")
+ with urllib.request.urlopen(request, timeout=60) as response:
+ return json.load(response)
+
+
+def resolve_release(releases, channel):
+ """The release a channel's filters select, or None if that channel is empty.
+
+ Mirrors the subset of Obtainium's selection we rely on: honour
+ includePrereleases, apply the release-title filter, skip drafts, and take the
+ newest match. Obtainium walks its own sorted list, but for a published-channel
+ probe "newest title match" is the same answer β confirmed on-device.
+ """
+ settings = channel["settings"]
+ include_prereleases = settings.get("includePrereleases", False)
+ title_filter = settings.get("filterReleaseTitlesByRegEx")
+ matches = []
+ for release in releases:
+ if release.get("draft"):
+ continue
+ if release.get("prerelease") and not include_prereleases:
+ continue
+ title = (release.get("name") or release.get("tag_name") or "").strip()
+ if title_filter and not re.search(title_filter, title):
+ continue
+ matches.append(release)
+ if not matches:
+ return None
+ return max(matches, key=lambda r: r.get("published_at") or "")
+
+
+def matching_assets(release, channel, flavor_key):
+ """Asset names in `release` that this channel/flavor's apkFilterRegEx selects."""
+ pattern = json.loads(config_for(channel, flavor_key)["additionalSettings"])[
+ "apkFilterRegEx"
+ ]
+ return [
+ asset["name"]
+ for asset in release.get("assets", [])
+ if asset["name"].lower().endswith((".apk", ".xapk", ".apkm", ".apks"))
+ and re.search(pattern, asset["name"])
+ ]
+
+
+def probe(releases):
+ """Resolve every channel/flavor against live releases.
+
+ Returns (rows, problems): rows feed the generated status table, problems are
+ regexes that no longer match anything in a release that does exist β i.e. the
+ release assets were renamed and these configs are broken.
+ """
+ rows, problems = [], []
+ for channel in CHANNELS:
+ release = resolve_release(releases, channel)
+ counts = {}
+ if release is not None:
+ for flavor_key in FLAVORS:
+ assets = matching_assets(release, channel, flavor_key)
+ counts[flavor_key] = len(assets)
+ if not assets:
+ problems.append(
+ f"{channel['key']}/{flavor_key}: apkFilterRegEx matched no "
+ f"APK in {release.get('tag_name')} β release assets renamed?"
+ )
+ rows.append({"channel": channel, "release": release, "counts": counts})
+ return rows, problems
+
+
+def render_status(rows):
+ """The live channel table. README only β never the archived/bundled guide."""
+ lines = [STATUS_BEGIN, "", "| Channel | Currently | Released |", "|---|---|---|"]
+ for row in rows:
+ channel, release = row["channel"], row["release"]
+ label = README_LABELS.get(channel["key"], channel["label"])
+ if release is None:
+ lines.append(f"| {label} | *none published right now* | β |")
+ continue
+ # Snapshot is rebuilt per push, so its tag is not a useful "version".
+ if channel.get("debug"):
+ version = "rolling, rebuilt on every push to `main`"
+ else:
+ version = f"`{release.get('tag_name', '?')}`"
+ date = (release.get("published_at") or "")[:10] or "β"
+ lines.append(f"| {label} | {version} | {date} |")
+ lines += ["", STATUS_END]
+ return "\n".join(lines)
+
+
+def render_export(flavor_key):
+ by_key = {c["key"]: c for c in CHANNELS}
+ # One release entry β this file's flavor β plus every debug flavor, which can
+ # all coexist thanks to their per-flavor application-ID suffixes.
+ apps = [config_for(by_key[EXPORT_RELEASE_CHANNEL], flavor_key)]
+ for key in EXPORT_DEBUG_CHANNELS:
+ apps += [config_for(by_key[key], f) for f in FLAVORS]
+ # Legacy `{apps, settings}` shape on purpose: Obtainium reads it via the
+ # schemaVersion-absent branch, so we avoid inventing exportedAt/appVersion
+ # provenance for a file no Obtainium install actually produced.
+ return json.dumps({"apps": apps, "settings": None}, indent=4) + "\n"
+
+
+def export_path(flavor_key):
+ return OBTAINIUM_DIR / f"meshtastic-obtainium-export-{flavor_key}.json"
+
+
+README_LABELS = {"stable": "**Latest release**", "open": "**Open beta**"}
+
+
+def targets(status_rows=None):
+ """Map of path -> expected full file content.
+
+ `status_rows` is supplied only by --refresh; without it the live status block
+ is left exactly as committed, so --check stays offline and deterministic.
+ """
+ out = {export_path(k): render_export(k) for k in FLAVORS}
+
+ # The guide is archived per release tag and bundled into the app, so it gets
+ # only the deep links β those derive from constants and never go stale. Live
+ # release state goes in the README alone, which GitHub always renders from the
+ # default branch.
+ out[DOC] = replace_block(DOC.read_text(), render_table())
+
+ readme_channels = [c for c in CHANNELS if c["key"] in README_CHANNELS]
+ readme = replace_block(
+ README.read_text(),
+ render_table(readme_channels, label_of=lambda c: README_LABELS[c["key"]]),
+ )
+ if status_rows is not None:
+ readme = replace_block(
+ readme,
+ render_status([r for r in status_rows if r["channel"]["key"] in README_CHANNELS]),
+ STATUS_BEGIN,
+ STATUS_END,
+ )
+ out[README] = readme
+ return out
+
+
def main():
parser = argparse.ArgumentParser()
- parser.add_argument("--json", action="store_true", help="print configs, not links")
+ parser.add_argument(
+ "--check", action="store_true", help="verify generated files are current"
+ )
+ parser.add_argument(
+ "--refresh",
+ action="store_true",
+ help="also probe the live releases API and refresh the channel status table",
+ )
args = parser.parse_args()
- if args.json:
- for label, app_id, name, settings in CHANNELS:
- print(f"# {label}")
- print(json.dumps(config_for(app_id, name, settings), separators=(",", ":")))
+ status_rows = None
+ if args.refresh:
+ try:
+ releases = fetch_releases()
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as err:
+ # A flaky API must not be reported as "configs are broken"; leave the
+ # committed status block alone and let the next run pick it up.
+ print(f"::warning::releases API unreachable ({err}); skipping --refresh")
+ return 0
+ status_rows, problems = probe(releases)
+ for problem in problems:
+ print(f"::error::{problem}")
+ if problems:
+ print(
+ "An apkFilterRegEx no longer matches the published assets. Either the "
+ "release workflow renamed them, or FLAVORS needs updating."
+ )
+ return 1
+
+ try:
+ expected = targets(status_rows)
+ except ValueError:
+ print(f"error: {DOC} is missing the BEGIN/END GENERATED markers")
+ return 1
+
+ # A missing file counts as drifted rather than crashing, so --check works on
+ # a fresh checkout and the first run can create the export files.
+ drifted = [
+ p
+ for p, content in expected.items()
+ if not p.exists() or p.read_text() != content
+ ]
+
+ if args.check:
+ for path in drifted:
+ print(f"drifted: {path.relative_to(REPO_ROOT)}")
+ if drifted:
+ print("run: python3 obtainium/generate-links.py")
+ return 1
+ print(f"up to date: {len(expected)} generated file(s)")
return 0
- print("| Channel | Link |")
- print("|---|---|")
- for label, app_id, name, settings in CHANNELS:
- blob = json.dumps(config_for(app_id, name, settings), separators=(",", ":"))
- # Only the JSON payload is percent-encoded; the scheme prefix stays literal.
- scheme = "obtainium://app/" + quote(blob, safe="")
- print(f"| {label} | [Add to Obtainium]({REDIRECT}{scheme}) |")
+ for path, content in expected.items():
+ path.write_text(content)
+ print(f"wrote: {path.relative_to(REPO_ROOT)}")
return 0
diff --git a/obtainium/meshtastic-obtainium-export-fdroid.json b/obtainium/meshtastic-obtainium-export-fdroid.json
new file mode 100644
index 0000000000..de728ed21a
--- /dev/null
+++ b/obtainium/meshtastic-obtainium-export-fdroid.json
@@ -0,0 +1,26 @@
+{
+ "apps": [
+ {
+ "id": "com.geeksville.mesh",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic",
+ "additionalSettings": "{\"apkFilterRegEx\":\"fdroid-.*-release\\\\.apk$\",\"autoApkFilterByArch\":true,\"appName\":\"Meshtastic\"}"
+ },
+ {
+ "id": "com.geeksville.mesh.google.debug",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic Snapshot (google)",
+ "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"google-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true,\"appName\":\"Meshtastic Snapshot (google)\"}"
+ },
+ {
+ "id": "com.geeksville.mesh.fdroid.debug",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic Snapshot (fdroid)",
+ "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"fdroid-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true,\"appName\":\"Meshtastic Snapshot (fdroid)\"}"
+ }
+ ],
+ "settings": null
+}
diff --git a/obtainium/meshtastic-obtainium-export-google.json b/obtainium/meshtastic-obtainium-export-google.json
new file mode 100644
index 0000000000..05d3355a85
--- /dev/null
+++ b/obtainium/meshtastic-obtainium-export-google.json
@@ -0,0 +1,26 @@
+{
+ "apps": [
+ {
+ "id": "com.geeksville.mesh",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic",
+ "additionalSettings": "{\"apkFilterRegEx\":\"google-release\\\\.apk$\",\"appName\":\"Meshtastic\"}"
+ },
+ {
+ "id": "com.geeksville.mesh.google.debug",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic Snapshot (google)",
+ "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"google-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true,\"appName\":\"Meshtastic Snapshot (google)\"}"
+ },
+ {
+ "id": "com.geeksville.mesh.fdroid.debug",
+ "url": "https://github.com/meshtastic/Meshtastic-Android",
+ "author": "meshtastic",
+ "name": "Meshtastic Snapshot (fdroid)",
+ "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"fdroid-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true,\"appName\":\"Meshtastic Snapshot (fdroid)\"}"
+ }
+ ],
+ "settings": null
+}
diff --git a/obtainium/meshtastic-obtainium-export.json b/obtainium/meshtastic-obtainium-export.json
deleted file mode 100644
index 1a402b4e4b..0000000000
--- a/obtainium/meshtastic-obtainium-export.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "apps": [
- {
- "id": "com.geeksville.mesh",
- "url": "https://github.com/meshtastic/Meshtastic-Android",
- "author": "meshtastic",
- "name": "Meshtastic",
- "additionalSettings": "{\"apkFilterRegEx\":\"google-release\\\\.apk$\"}"
- },
- {
- "id": "com.geeksville.mesh.google.debug",
- "url": "https://github.com/meshtastic/Meshtastic-Android",
- "author": "meshtastic",
- "name": "Meshtastic Snapshot",
- "additionalSettings": "{\"includePrereleases\":true,\"filterReleaseTitlesByRegEx\":\"^Snapshot\",\"useLatestAssetDateAsReleaseDate\":true,\"versionDetection\":false,\"releaseDateAsVersion\":true,\"apkFilterRegEx\":\"google-.*-debug-\\\\d+\\\\.apk$\",\"autoApkFilterByArch\":true}"
- }
- ],
- "settings": null
-}
Served by rngit 1.5.2 - Generated in 0.21s